﻿/***************
 * Copyright 2009 (C) 
 * Timur Fanshteyn
 * Blog: http://blog.tfanshteyn.com
 * ************/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using Tangosol.IO.Pof;
using System.Collections;
using Tangosol.Net;

namespace Coherence.Contrib
{
    internal class GenericSerializer<T> : Tangosol.IO.Pof.IPofSerializer where T : new()
    {
        private struct WriteAsTypeResult
        {
            public Type OriginalType;
            public POFWriteAsTypeEnum WriteAsType;
        }

        private List<Action<T, IPofWriter>> WriteSerializer = new List<Action<T, IPofWriter>>();
        private List<Action<T, IPofReader>> ReadSerializer = new List<Action<T, IPofReader>>();
        private int typeId;
        private bool StoreMetadata;
        private string[] MetaData;

        public GenericSerializer(int typeId)
        {
            this.typeId = typeId;
            Type t = typeof(T);
            POFSerializableObjectAttribute objAttrib = (POFSerializableObjectAttribute) Attribute.GetCustomAttribute(t, typeof(POFSerializableObjectAttribute));
            if (objAttrib == null)
                throw new InvalidOperationException("type must have POFSerializableObjectAttribute");
            StoreMetadata = objAttrib.StoreMetadata;

            List<MemberInfo> members = new List<MemberInfo>();
            members.AddRange(t.GetProperties().Where(prop =>
                prop.CanRead && prop.CanWrite).ToArray());
            members.AddRange(t.GetFields().Where(field =>
                field.IsPublic && !field.IsStatic).ToArray());
            var goodMembers = (from mi in members
                               let Attrib = (POFSerializableMemberAttribute) Attribute.GetCustomAttribute(mi, typeof(POFSerializableMemberAttribute))
                               where Attrib!=null
                               orderby Attrib.Order==-1 ? int.MaxValue : Attrib.Order,
                               mi.Name
                               select mi) .ToArray();

            int startPosition = 0;
            if (StoreMetadata)
            {
                startPosition = 1;
                MetaData = GetMetadataArray(goodMembers);
            }
            CreateSerializer(startPosition, goodMembers);


            //if (objAttrib.StoreMetadata)
            //    StoreMetadata(goodMembers);
        }

        private string[] GetMetadataArray(MemberInfo[] goodMembers)
        {
            return goodMembers.Select(m=>m.Name).ToArray();
        }

        private void CreateSerializer(int startPosition, MemberInfo[] goodMembers)
        {
            int cnt = startPosition;
            foreach (MemberInfo Member in goodMembers)
            {
                int i = cnt;
                MemberInfo mi = Member;
                Action<T, IPofWriter> write;
                Action<T, IPofReader> read;

                WriteAsTypeResult writeAs = GetWriteAsType(mi);
                switch (writeAs.WriteAsType)
                {
                    case POFWriteAsTypeEnum.Array:
                        write = (p, r) => { Array val = (Array) GetValue(mi, p); r.WriteArray(i, val); };
                        read = (p, r) => { Array val = r.ReadArray(i); SetValue(mi, p, val); };
                        break;
                    case POFWriteAsTypeEnum.Boolean:
                        write = (p, r) => { bool val = (bool)GetValue(mi, p); r.WriteBoolean(i, val); };
                        read = (p, r) => { bool val = r.ReadBoolean(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.Byte:
                        write = (p, r) => { byte val = (byte)GetValue(mi, p); r.WriteByte(i, val); };
                        read = (p, r) => { byte val = r.ReadByte(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.Char:
                        write = (p, r) => { char val = (char)GetValue(mi, p); r.WriteChar(i, val); };
                        read = (p, r) => { char val = r.ReadChar(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.CharAray:
                        write = (p, r) => { char[] val = (char[])GetValue(mi, p); r.WriteCharArray(i, val); };
                        read = (p, r) => { char[] val = r.ReadCharArray(i); SetValue(mi, p, val); };
                        break;
                    case POFWriteAsTypeEnum.Date:
                        write = (p, r) => { DateTime val = (DateTime)GetValue(mi, p); r.WriteDate(i, val); };
                        read = (p, r) => { DateTime val = r.ReadDate(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.DateTime:
                        write = (p, r) => { DateTime val = (DateTime)GetValue(mi, p); r.WriteDateTime(i, val); };
                        read = (p, r) => { DateTime val = r.ReadDateTime(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.DayTimeInterval:
                        write = (p, r) => { TimeSpan val = (TimeSpan)GetValue(mi, p); r.WriteDayTimeInterval(i, val); };
                        read = (p, r) => { TimeSpan val = r.ReadDayTimeInterval(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.Decimal:
                        write = (p, r) => { decimal val = (decimal)GetValue(mi, p); r.WriteDecimal(i, val); };
                        read = (p, r) => { decimal val = r.ReadDecimal(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.Double:
                        write = (p, r) => { double val = Convert.ToDouble(GetValue(mi, p)); r.WriteDouble(i, val); };
                        read = (p, r) => { double val = r.ReadDouble(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.Int16:
                        write = (p, r) => { Int16 val = Convert.ToInt16(GetValue(mi, p)); r.WriteInt16(i, val); };
                        read = (p, r) => { Int16 val = r.ReadInt16(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.Int32:
                        write = (p, r) => { Int32 val = Convert.ToInt32(GetValue(mi, p)); r.WriteInt32(i, val); };
                        read = (p, r) => { Int32 val = r.ReadInt32(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.Int64:
                        write = (p, r) => { Int64 val = Convert.ToInt64(GetValue(mi, p)); r.WriteInt64(i, val); };
                        read = (p, r) => { Int64 val = r.ReadInt64(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.LocalDateTime:
                        write = (p, r) => { DateTime val = (DateTime)GetValue(mi, p); r.WriteLocalDateTime(i, val); };
                        read = (p, r) => { DateTime val = r.ReadLocalDateTime(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.LocalTime:
                        write = (p, r) => { DateTime val = (DateTime)GetValue(mi, p); r.WriteLocalTime(i, val); };
                        read = (p, r) => { DateTime val = r.ReadLocalDateTime(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.Object:
                        write = (p, r) => { object val = (object)GetValue(mi, p); r.WriteObject(i, val); };
                        read = (p, r) => { object val = r.ReadObject(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.Single:
                        write = (p, r) => { float val = (float)GetValue(mi, p); r.WriteSingle(i, val); };
                        read = (p, r) => { float val = r.ReadSingle(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.String:
                        write = (p, r) => { string val = (string)GetValue(mi, p); r.WriteString(i, val); };
                        read = (p, r) => { string val = r.ReadString(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.Time:
                        write = (p, r) => { DateTime val = (DateTime)GetValue(mi, p); r.WriteTime(i, val); };
                        read = (p, r) => { DateTime val = r.ReadDateTime(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.TimeInterval:
                        write = (p, r) => { TimeSpan val = (TimeSpan)GetValue(mi, p); r.WriteTimeInterval(i, val); };
                        read = (p, r) => { TimeSpan val = r.ReadTimeInterval(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.UniversalDateTime:
                        write = (p, r) => { DateTime val = (DateTime)GetValue(mi, p); r.WriteUniversalDateTime(i, val); };
                        read = (p, r) => { DateTime val = r.ReadUniversalDateTime(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    case POFWriteAsTypeEnum.UniversalTime:
                        write = (p, r) => { DateTime val = (DateTime)GetValue(mi, p); r.WriteUniversalTime(i, val); };
                        read = (p, r) => { DateTime val = r.ReadUniversalDateTime(i); SetValue(mi, p, Convert.ChangeType(val, writeAs.OriginalType)); };
                        break;
                    default:
                        throw new NotSupportedException("WriteAsType " + writeAs.ToString() + " not supported");
                }
                WriteSerializer.Add(write);
                ReadSerializer.Add(read);

                cnt++;
            }
        }

        private static object GetValue(MemberInfo mi, T p)
        {
            if (mi as PropertyInfo != null)
                return ((PropertyInfo)mi).GetValue(p, null);
            else
                return ((FieldInfo)mi).GetValue(p);
        }

        private static void SetValue(MemberInfo mi, T p, object value)
        {
            if (mi as PropertyInfo != null)
                ((PropertyInfo)mi).SetValue(p, value, null);
            else
                ((FieldInfo)mi).SetValue(p, value);
        }

        private WriteAsTypeResult GetWriteAsType(MemberInfo mi)
        {
            POFSerializableMemberAttribute attrib = (POFSerializableMemberAttribute)mi.GetCustomAttributes(typeof(POFSerializableMemberAttribute), true)[0];
            
            Type PropType;
            if (mi as PropertyInfo!=null)
                PropType = ((PropertyInfo)mi).PropertyType;
            else
                PropType = ((FieldInfo)mi).FieldType;

            if (attrib.WriteAsType != POFWriteAsTypeEnum.Default)
                return new WriteAsTypeResult() { OriginalType = PropType, WriteAsType = attrib.WriteAsType };

            POFWriteAsTypeEnum WriteAsType;
            if (typeof(Int16).IsAssignableFrom(PropType)) { WriteAsType = POFWriteAsTypeEnum.Int16; }
            if (typeof(Int32).IsAssignableFrom(PropType)) { WriteAsType = POFWriteAsTypeEnum.Int32; }
            else if (typeof(Int64).IsAssignableFrom(PropType)) { WriteAsType = POFWriteAsTypeEnum.Int64; }
            else if (typeof(string).IsAssignableFrom(PropType)) { WriteAsType = POFWriteAsTypeEnum.String; }
            else if (typeof(bool).IsAssignableFrom(PropType)) { WriteAsType = POFWriteAsTypeEnum.Boolean; }
            else if (typeof(byte).IsAssignableFrom(PropType)) { WriteAsType = POFWriteAsTypeEnum.Byte; }
            else if (typeof(DateTime).IsAssignableFrom(PropType)) { WriteAsType = POFWriteAsTypeEnum.DateTime; }
            else if (typeof(decimal).IsAssignableFrom(PropType)) { WriteAsType = POFWriteAsTypeEnum.Decimal; }
            else if (typeof(double).IsAssignableFrom(PropType)) { WriteAsType = POFWriteAsTypeEnum.Double; }
            else if (typeof(float).IsAssignableFrom(PropType)) { WriteAsType = POFWriteAsTypeEnum.Single; }
            else if (typeof(TimeSpan).IsAssignableFrom(PropType)) { WriteAsType = POFWriteAsTypeEnum.DayTimeInterval; }
            else
            throw new InvalidOperationException("You must specify WriteAsType attribute for type " + PropType.ToString());

            return new WriteAsTypeResult() { OriginalType = PropType, WriteAsType = WriteAsType };
        }

        #region IPofSerializer Members

        public object Deserialize(Tangosol.IO.Pof.IPofReader reader)
        {
            if (StoreMetadata)
                reader.ReadArray(0);

            T obj = new T();
            foreach (var action in ReadSerializer)
            {
                action(obj, reader);
            }
            reader.ReadRemainder();
            return obj;
        }

        public void Serialize(Tangosol.IO.Pof.IPofWriter writer, object o)
        {
            if (StoreMetadata)
                writer.WriteArray(0, MetaData);
            foreach (var action in WriteSerializer)
            {
                action((T)o, writer);
            }
            writer.WriteRemainder(null);
        }

        #endregion
    }

}